import std.concurrency, std.stdio, std.exception, std.variant;

import Definitions.msgCatalogue;
import Definitions.ozyConfig;
import Definitions.procNames;
import Registry.registry;

extern (C) int open_dev();
extern (C) int bulk_read(int endpoint, char *buf, int size, int timeout);
extern (C) int bulk_write(int endpoint, char *buf, int size, int timeout);

// Constants
// Default frame and buffer size (IQ samples are interleaved)
auto DSP_IQ_FRAMES = 512;
int DSP_IQ_FRAME_SZ;
// Float frames within a single block read
immutable auto OZY_IQ_FRAME_SZ = 63;
// Protocol byte block size
immutable auto OZY_READ_FRAME_SZ = 512;
immutable auto OZY_WRITE_FRAME_SZ = 512;
// Endpoints EP2, EP4 and EP6 and transfer timeout
immutable auto ENDPOINT_EP2 = 0x2;
immutable auto ENDPOINT_EP4 = 0x4 | 0x80;
immutable auto ENDPOINT_EP6 = 0x6 | 0x80;
immutable auto TIMEOUT = 30;
// Offset from start of buffer to data (allows for sync and CC
immutable auto CONTROL_OFFSET = 8;

Receiver oreceiver;
Transmitter otransmitter;

//===========================================================================================
int open_hpsdr()
{
    // Open the HPSDR device
    return open_dev();

}

Tid init_hpsdr()
{
    // The HPSDR receiver thread
    // Send received data to the receiver TID
    // Returns the TID to send rx output audio to

    auto hpsdr_tid = spawn(&trxout);
    auto rx_tid = spawn(&trxin);

    return hpsdr_tid;
}

void trxin() {

    // Create the class that will read rx blocks from the device.
    writeln("Creating receiver");
    oreceiver = new Receiver();
    oreceiver.process();
}

void trxout() {

    // Create the class that will write the audio out blocks to the device.
    writeln("Creating transmitter");
    otransmitter = new Transmitter();
    otransmitter.process();
}

//===========================================================================================
class Receiver {

    // Constants
    // Scale the input from a 24 bit value to a double 0..1
    immutable double input_scale = 2^^23 - 1.0;

    // variables
    // Double buffered to allow continuous read of device
    float[] smpls;
    // Block buffer for reading from device
    ubyte[] buf;
    // Pointer into circular buffer
    auto w_ptr = 0;
    auto r_ptr = -1;
    // Holder for integer value translated from buffer
    int as_int;
    // Holder for double value translated from integer value
    double as_double;
    // Index into buffer
    auto index = 0;

    // Constructor
    this() {
        // Double buffered output for interleaved samples
        DSP_IQ_FRAME_SZ = DSP_IQ_FRAMES*2;
        this.smpls = new float[DSP_IQ_FRAME_SZ*2];
        // Single read buffer input
        this.buf = new ubyte[OZY_READ_FRAME_SZ];
    }

    // Read from the device and block up to a buffer full then send it to the main_tid
    void process() {

        writeln("Receiver processing");
        while (true) {
            // Read and translate one block into the buffer
            auto sz = bulk_read(ENDPOINT_EP6, cast(char*)this.buf, OZY_READ_FRAME_SZ, TIMEOUT);
            //writeln("Read data: ", this.buf);
            //writeln("Read block");
            if (sz == OZY_READ_FRAME_SZ) {
                // Collect a frame size of floats from the raw buffer
                foreach (i; 0 .. OZY_IQ_FRAME_SZ) {
                    // Collect I and Q 24 bit values for each sample
                    // Allow for offset and step through by 8 bytes (3xI + 3xQ + 2xMic)
                    index = (i*8) + CONTROL_OFFSET;
                    // If the top bit is set then prime the receiver to be negative else 0
                    foreach(j; 0 .. 2) {
                        // I is 0 and Q is 1
                        if (this.buf[index + (j*3)] & 0x80) {
                            as_int = -1;
                        }
                        else {
                            as_int = 0;
                        }
                        // Move the 24 bit sample into an integer representation
                        as_int = (as_int | (this.buf[index+(j*3)] & 0xFF)) << 8;
                        as_int = (as_int | (this.buf[index+(j*3)+1] & 0xFF)) << 8;
                        as_int = as_int | (this.buf[index+(j*3)+2] & 0xFF);
                        // Scale the result to a double with range +-1
                        as_double = (cast(double)as_int)/input_scale;
                        // Add into the sample buffer as a float
                        this.smpls[w_ptr++] = cast(float)as_double;
                        if (w_ptr > this.smpls.length -1) {
                            w_ptr = 0;
                            // We have passed the bottom of the buffer so frame 1 is ready to go
                            r_ptr = 1;
                        }
                        else if (w_ptr == this.smpls.length/2) {
                            // We have reached the half way point of the buffer so frame 0 is ready to go
                            r_ptr = 0;
                        }
                        else {
                            // Some between state
                            r_ptr = -1;
                        }
                    }
                    // See if there is anything to dispatch
                    if (r_ptr != -1) {
                        //writeln("Dispatching...");
                        // Yes, so allocate a transfer buffer
                        //float[] xfer = new float[DSP_IQ_FRAME_SZ];
                        // Get the appropriate slice
                        immutable(float)[] xfer = this.smpls[r_ptr*DSP_IQ_FRAME_SZ .. (r_ptr*DSP_IQ_FRAME_SZ) + DSP_IQ_FRAME_SZ].idup;
                        // Copy to an immutable buffer and send
                        //writeln("About to send: ", xfer);
                        CRegistry.getTid(E_PROC.MAIN).send(M_SMPLS_IN(), xfer);
                        //writeln("Data sent");
                    }
                }
            }
            else {
                writeln("Bad read from device,ignored");
            }
        }
    }
}

//===========================================================================================
class Transmitter {

    // Constants
    immutable double output_scale = (2 ^^ 15) - 1.0;

    // variables
    // Double buffered to allow continuous write of device
    float[] smpls_buf;
    // Block buffer for writing to device
    ubyte[] buf;
    // Pointer into circular buffer
    auto w_ptr = 0;
    auto r_ptr = 0;
    // Holders for sample values
    auto left_as_int = 0;
    auto right_as_int = 0;

    OutputFrame output_buffer;

    // Constructor
    this() {

        // Create an instance of the output buffer
        this.output_buffer = new OutputFrame();
        //this.dispatch_table["rate"] = Variant(&this.output_buffer.set_rate);

        // Circular double buffer to accumulate received processed frames
        DSP_IQ_FRAME_SZ = DSP_IQ_FRAMES*2;
        this.smpls_buf = new float[DSP_IQ_FRAME_SZ*2];
        // Output buffer to device
        this.buf = new ubyte[OZY_WRITE_FRAME_SZ];

        // Write a few frames without data to kick hpsdr into life
        foreach(i; 0 .. 1) {
            buf = this.output_buffer.get_buffer_frame();
            bulk_write(ENDPOINT_EP2, cast(char *)buf, OZY_WRITE_FRAME_SZ, TIMEOUT);
        }
    }

    void process() {

        writeln("Transmitter processing");
        while (true) {
            receive(
                // Sample output processing
                (M_SMPLS_OUT discriminator, immutable (float) [] smpls) {
                    //writeln("Transmitter received data ", smpls);
                    //writeln("M_SMPLS_OUT");
                    add_to_buffer(smpls);
                    output_data();
                },
                // HPSDR configuration changes
                &this.output_buffer.set_rate,
                &this.output_buffer.set_clock_src1,
                &this.output_buffer.set_clock_src2,
                &this.output_buffer.set_board_config,
                &this.output_buffer.set_mic_connection,
                &this.output_buffer.set_freq,
                &this.output_buffer.inc_freq
            );
        }
    }

    void add_to_buffer(immutable(float)[] smpls) {

        try {
            // Copy into the buffer at the w_ptr offset
            if (w_ptr+smpls.length > this.smpls_buf.length) {
                // Split transfer
                this.smpls_buf[w_ptr .. $] = smpls[0 .. this.smpls_buf.length - w_ptr].idup;
                // We wrote from w_ptr to the end of the buffer
                auto written = this.smpls_buf.length - w_ptr;
                // Reset w_ptr to the start of the buffer
                w_ptr = 0;
                // Write the remainder from the start of the buffer
                this.smpls_buf[w_ptr .. smpls.length - written] = smpls[written .. $].idup;
                // Update w_ptr
                w_ptr = smpls.length - written;
            }
            else {
                // Straight transfer
                this.smpls_buf[w_ptr .. w_ptr+smpls.length] = smpls.idup;
                w_ptr = w_ptr+smpls.length;
            }
            //writeln(w_ptr);
        }
        catch (Exception e) {
            writeln("Exception add_to_buffer: ", e);
        }
    }

    void output_data() {

        try {
            auto sz = 0;
            // Check if we have data to output
            if (r_ptr < w_ptr) {
                sz = w_ptr-r_ptr;
            }
            else {
                sz = (this.smpls_buf.length - r_ptr) + w_ptr;
            }

            if (sz > OZY_IQ_FRAME_SZ*2) {
                // We have enough IQ interleaved sampls to send a frame
                // We have data to send
                // Send the number of integral blocks we can get out of sz
                //writeln("Sending ", cast(int)(sz/(OZY_IQ_FRAME_SZ*2)), " blocks");
                foreach(i; 0 .. cast(int)(sz/(OZY_IQ_FRAME_SZ*2))) {
                    // Construct a frame with the correct control bytes to add samples to
                    //writeln("Sending block no. ", i);
                    buf = this.output_buffer.get_buffer_frame();
                    // Construct and send one frame
                    foreach(j; 0 .. OZY_IQ_FRAME_SZ) {
                        // Assemble one I/Q sample
                        // get the right and left samples from the buffer
                        r_ptr = r_ptr + 2;
                        if(r_ptr > this.smpls_buf.length-1) {
                            r_ptr = 0;
                        }
                        left_as_int = cast(int)((this.smpls_buf[r_ptr])*output_scale);
                        right_as_int = cast(int)((this.smpls_buf[r_ptr+1])*output_scale);
                        //writeln("Raw data: ", this.smpls_buf[r_ptr], " ", left_as_int, " ", this.smpls_buf[r_ptr+1], " ", right_as_int);
                        // Move up in 8 byte steps (L*2, R*2, IQ TX * 4)
                        // Currently only the L & R audio outputs are written
                        buf[CONTROL_OFFSET+(j*8)] = left_as_int >> 8 & 0xff;
                        buf[CONTROL_OFFSET+(j*8)+1] = left_as_int & 0xff;
                        buf[CONTROL_OFFSET+(j*8)+2] = right_as_int >> 8 & 0xff;
                        buf[CONTROL_OFFSET+(j*8)+3] = right_as_int & 0xff;
                    }

                    // Output the frame
                    //writeln("Output data", buf);
                    bulk_write(ENDPOINT_EP2, cast(char *)buf, OZY_WRITE_FRAME_SZ, TIMEOUT);
                }
            }
        }
        catch (Exception e) {
            writeln("Exception in output_data: ", e);
        }
    }
}

//===========================================================================================
class OutputFrame {

    // Constants
    // Control byte definitions
    immutable auto  SPEED_48 = 0x00;
    immutable auto  SPEED_96 = 0x01;
    immutable auto  SPEED_192 = 0x02;
    immutable auto  MASK_SPEED = 0xFC;
    immutable auto  REF_ATLAS = 0x00;
    immutable auto  REF_PEN = 0x04;
    immutable auto  REF_MERC = 0x08;
    immutable auto  MASK_REF = 0xF3;
    immutable auto  SRC_PEN = 0x00;
    immutable auto  SRC_MERC = 0x10;
    immutable auto  MASK_SRC = 0xEF;
    immutable auto  CONFIG_NIL = 0x00;
    immutable auto  CONFIG_PEN = 0x20;
    immutable auto  CONFIG_MERC = 0x40;
    immutable auto  CONFIG_BOTH = 0x60;
    immutable auto  MASK_CONFIG = 0x9F;
    immutable auto  MIC_JAN = 0x00;
    immutable auto  MIC_PEN = 0x80;
    immutable auto  MASK_MIC = 0x7F;
    // Skip sync bytes
    immutable auto DATA_OFFSET = 3;

    // variables
    ubyte[] FRAME;
    ubyte[] SYNC;
    ubyte[] CONTROL;
    ubyte[] FREQ;
    auto toggle = 0;

    double freq = 0.0;

    // Constructor
    this() {
        // Create the frame and default control and sync bytes
        this.FRAME = new ubyte[OZY_WRITE_FRAME_SZ];
        this.SYNC =  cast(ubyte [])[0x7f, 0x7f, 0x7f];
        this.CONTROL =  cast(ubyte [])[0x00, 0xE4, 0x00, 0x00, 0x00];
        this.FREQ = cast(ubyte [])[0x02, 0x00, 0x00, 0x00, 0x00];
    }

    // Retuen a frame with sync bytes and alternate control and freq bytes
    // The remainder is to be filled by the receiver
    // This buffer is reused and we should make the sync bytes immutable (somehow)
    // and not reset them each time
    ubyte [] get_buffer_frame() {
        this.FRAME[0 .. this.SYNC.length] = this.SYNC;
        if (toggle == 0) {
            this.FRAME[DATA_OFFSET .. this.CONTROL.length+DATA_OFFSET] = this.CONTROL;
            this.toggle = 1;
        }
        else if (toggle == 1) {
            this.FRAME[DATA_OFFSET .. this.FREQ.length+DATA_OFFSET] = this.FREQ;
            this.toggle = 0;
        }
        return this.FRAME;
    }

    // Set the CONTROL or FREQ data as required in the output frame
    // Settings are automatically synchronised because of the message handling

    //-------------------------------------------------------------------------------
    // MOX on/off
    // boolean - true = on
    void set_mox() {
    }

    //-------------------------------------------------------------------------------
    // Speed
    // Enumeration
    // KHz48, KHz96, KHz192 ...
    void set_rate(M_RATE discriminator, E_SPEED speed) {
        //writeln("set_rate");

        if (speed == E_SPEED.KHz48) {
            this.CONTROL[1] &= MASK_SPEED;
            this.CONTROL[1] |= SPEED_48;
        }
        else if (speed == E_SPEED.KHz96) {
            this.CONTROL[1] &= MASK_SPEED;
            this.CONTROL[1] |= SPEED_96;
        }
        else if (speed == E_SPEED.KHz192) {
            this.CONTROL[1] &= MASK_SPEED;
            this.CONTROL[1] |= SPEED_192;
        }
    }

    //-------------------------------------------------------------------------------
    // 10 MHz ref
    // Enumeration
    // Atlas, Penelope, Mercury.
    void set_clock_src1(M_SRC_1 discriminator, E_CLOCK_LO_REF clock_lo_ref) {

        //writeln("set_clock_src1");
        if (clock_lo_ref == E_CLOCK_LO_REF.Atlas) {
            this.CONTROL[1] &= MASK_REF;
            this.CONTROL[1] |= REF_ATLAS;
        }
        else if (clock_lo_ref == E_CLOCK_LO_REF.Clk_10MHZ_Penelope) {
            this.CONTROL[1] &= MASK_REF;
            this.CONTROL[1] |= REF_PEN;
        }
        else if (clock_lo_ref == E_CLOCK_LO_REF.Clk_10MHZ_Mercury) {
            this.CONTROL[1] &= MASK_REF;
            this.CONTROL[1] |= REF_MERC;
        }
    }

    //-------------------------------------------------------------------------------
    // 122.88 MHz ref source
    // Enumeration
    // Penelope, Mercury
    void set_clock_src2(M_SRC_2 discriminator, E_CLOCK_HI_REF clock_hi_ref) {

        //writeln("set_clock_src2");
        if (clock_hi_ref == E_CLOCK_HI_REF.Clk_122MHZ_Penelope) {
            this.CONTROL[1] &= MASK_SRC;
            this.CONTROL[1] |= SRC_PEN;
        }
        else if (clock_hi_ref == E_CLOCK_HI_REF.Clk_122MHZ_Mercury) {
            this.CONTROL[1] &= MASK_SRC;
            this.CONTROL[1] |= SRC_MERC;
        }
    }

    //-------------------------------------------------------------------------------
    // Board configuration
    // Enumeration
    // Nil, Penelope, Mercury, Both
    void set_board_config(M_BOARD_CONFIG discriminator, E_BOARD_CONFIG board_config) {

        //writeln("set_board_config");
        if (board_config == E_BOARD_CONFIG.None_Present) {
            this.CONTROL[1] &= MASK_CONFIG;
            this.CONTROL[1] |= CONFIG_NIL;
        }
        else if (board_config == E_BOARD_CONFIG.Penelope_Present) {
            this.CONTROL[1] &= MASK_CONFIG;
            this.CONTROL[1] |= CONFIG_PEN;
        }
        else if (board_config == E_BOARD_CONFIG.Mercury_Present) {
            this.CONTROL[1] &= MASK_CONFIG;
            this.CONTROL[1] |= CONFIG_MERC;
        }
        else if (board_config == E_BOARD_CONFIG.Both_Present) {
            this.CONTROL[1] &= MASK_CONFIG;
            this.CONTROL[1] |= CONFIG_BOTH;
        }
    }

    //-------------------------------------------------------------------------------
    // Microphone source
    // Enumeration
    // Janus,  Penelope
    void set_mic_connection(M_MIC_CONN discriminator, E_MIC_SOURCE mic_src) {

        //writeln("set_mic_connection");
        if (mic_src == E_MIC_SOURCE.Mic_Janus) {
            this.CONTROL[1] &= MASK_MIC;
            this.CONTROL[1] |= MIC_JAN;
        }
        else if (mic_src == E_MIC_SOURCE.Mic_Penelope) {
            this.CONTROL[1] &= MASK_MIC;
            this.CONTROL[1] |= MIC_PEN;
        }
    }

    //-------------------------------------------------------------------------------
    // Frequency set
    // float - freq
    void set_freq(M_SET_FREQ discriminator, double freq) {

        //writeln("set_freq: ", freq);
        this.freq = freq;
        long l_freq;

        // Load the frequency into the control bytes
        // Make this a freq in Hz
        freq = freq*1000000;
        // and convert to a long
        l_freq = cast(long)freq;

        this.FREQ[4] = l_freq & 0xFF;
        this.FREQ[3] = (l_freq >> 8) & 0xFF;
        this.FREQ[2] = (l_freq >> 16) & 0xFF;
        this.FREQ[1] = (l_freq >> 24) & 0xFF;

    }

    //-------------------------------------------------------------------------------
    // Frequency set
    // float - freq
    void inc_freq(M_INC_FREQ discriminator, double inc) {

        //writeln("set_freq: ", freq);
        this.freq = this.freq + inc;
        set_freq(M_SET_FREQ(), this.freq);
        //CRegistry.getTid(E_PROC.GUI).send(M_SET_FREQ(), freq);
        CRegistry.getTid(E_PROC.WEB).send(M_SET_FREQ(), freq);
    }
}
